<?php

class Server
{
    const SOCKS_AUTH = true;
    const SOCKS_VERSION = 5;
    const SOCKS_USERNAME = 'krasen';
    const SOCKS_PASSWORD = 'krasen';

    private $server;

    private $status = 'INIT';
    private $remote_addr;
    private $remote_port;

    private $host;
    private $port;

    public function __construct($host = '0.0.0.0', $port = 9501)
    {
        $this->host   = $host;
        $this->port   = $port;
        $this->server = new swoole_server($this->host, $this->port);
        $this->server->set(array(
            'worker_num'          => 8,
            'daemonize'           => false,
            'max_request'         => 10000,
            'dispatch_mode'       => 2,
            'package_max_length'  => 8192,
//            'open_length_check'     => true,
//            'package_length_offset' => 0,
//            'package_body_offset'   => 4,
            'package_length_type' => 'N'
        ));
        $this->server->on('Start', array($this, 'onStart'));
        $this->server->on('Connect', array($this, 'onConnect'));
        $this->server->on('Receive', array($this, 'onReceive'));
        $this->server->on('Close', array($this, 'onClose'));

        $this->server->start();
    }

    public function onStart($server)
    {
        echo sprintf('[%s] Start %s:%s' . PHP_EOL, self::udate(), $this->host, $this->port);
    }

    public function onConnect($server, $fd, $from_id)
    {
        echo sprintf('[%s] Client %s connect' . PHP_EOL, self::udate(), $fd);

    }

    public function onReceive(swoole_server $server, $fd, $from_id, $data)
    {
        if ('INIT' == $this->status) {
            if (!self::SOCKS_AUTH) {    //不需要验证，发送0500 十六进制
                $info = unpack("h1version/h1cmd/h1rsv/h1atyp", $data);
                $server->send($fd, pack('C2', 0x05, 0x00), $from_id);
                $this->status = 'BIND';
                echo sprintf('[%s] Client %s CMD bind' . PHP_EOL, self::udate(), $fd);
            }else{  //需要验证，发送0502
                echo sprintf('[%s] Client %s request need auth...' . PHP_EOL, self::udate(), $fd);
                $this->status = 'AUTH';
                $server->send($fd, pack('C2', 0x05, 0x02), $from_id);
            }
        } elseif ('BIND' == $this->status) {
            $info = unpack("h1version/h1cmd/h1rsv/h1atyp/H2addr1/H2addr2/H2addr3/H2addr4/H2port1/H2port2", $data);
            $server->send($fd, pack('C10', 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), $from_id);
            $this->status      = 'CONNECT';
            $this->remote_addr = implode('.', [hexdec($info['addr1']), hexdec($info['addr2']), hexdec($info['addr3']), hexdec($info['addr4'])]);
            $this->remote_port = intval(hexdec($info['port1']) . hexdec($info['port2']));
            echo sprintf('[%s] Client %s CMD CONNECT' . PHP_EOL, self::udate(), $fd);
        } elseif ('CONNECT' == $this->status) {
            echo sprintf('[%s] Client %s begin request data...' . PHP_EOL, self::udate(), $fd);
            $client = new swoole_client(SWOOLE_SOCK_TCP);
            if ($client->connect($this->remote_addr, $this->remote_port, -1)) {
                $client->send($data);
                $server->send($fd, $client->recv(), $from_id);
                echo sprintf('[%s] Client %s end request data...' . PHP_EOL, self::udate(), $fd);
                $this->status = 'INIT';
//                $server->close($fd);
                $client->close();
            }
        } elseif ('AUTH' == $this->status) {
            echo sprintf('[%s] Client %s request begin auth...' . PHP_EOL, self::udate(), $fd);
            $info = unpack("H2flag/H2u_length", $data);
            $length_username = dechex($info['u_length']) * 2;
            $info2 = unpack("H2flag/H2u_length/H{$length_username}username/H2p_length/H*password", $data);
            $username = hex2bin($info2['username']);
            $password = hex2bin($info2['password']);
            if(self::SOCKS_USERNAME == $username && self::SOCKS_PASSWORD == $password) {
                echo sprintf('[%s] Client %s request auth success...' . PHP_EOL, self::udate(), $fd);
                $server->send($fd, pack('C2', 0x01, 0x00));
                $this->status = 'BIND';
            }else{
                echo sprintf('[%s] Client %s request auth failed...' . PHP_EOL, self::udate(), $fd);
                $server->send($fd, pack('C2', 0x01, 0x05));
            }
        }
    }

    public function onClose($server, $fd, $from_id)
    {
        echo sprintf('[%s] Client %s close connection' . PHP_EOL, self::udate(), $fd);
    }

    public static function udate()
    {
        $arr = explode(' ', microtime());
        return date('Y-m-d H:i:s', $arr[1]) . '.' . round($arr[0] * 1000);
    }
}

new Server();